package com.shisan.tingshu.user.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.shisan.tingshu.common.constant.SystemConstant;
import com.shisan.tingshu.common.rabbit.constant.MqConst;
import com.shisan.tingshu.common.rabbit.service.RabbitService;
import com.shisan.tingshu.common.util.AuthContextHolder;
import com.shisan.tingshu.common.util.MongoUtil;
import com.shisan.tingshu.model.user.UserListenProcess;
import com.shisan.tingshu.user.service.UserInfoService;
import com.shisan.tingshu.user.service.UserListenProcessService;
import com.shisan.tingshu.vo.album.TrackStatMqVo;
import com.shisan.tingshu.vo.user.UserListenProcessVo;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
@SuppressWarnings({"unchecked", "rawtypes"})
@Slf4j
public class UserListenProcessServiceImpl implements UserListenProcessService {

	@Autowired
	private MongoTemplate mongoTemplate;

	@Autowired
	private RabbitService rabbitService;

	@Autowired
	private StringRedisTemplate redisTemplate;

	@Autowired
	private UserInfoService userInfoService;

	/**
	 * 查询已经登录用户的声音暂停秒数
	 * @param trackId
	 * @return
	 */
	@Override
	public BigDecimal getTrackBreakSecond(Long trackId) {

		Long userId = AuthContextHolder.getUserId();

		// 构建条件对象
		// Criteria是Java中用于构建查询条件的一个接口，常见于MongoDB的Java驱动（如Spring Data MongoDB）或类似的ORM框架中
		// 它提供了一种类型安全、流畅的API来构建查询条件，而不是直接写原生查询语句
		Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(trackId);

		// 构建查询对象，并将条件对象放到查询对象中
		Query query = new Query(criteria);

		// 利用查询对象，从MongoDB中查询数据
		/**
		 * findOne() 方法用于从 MongoDB 中查询单个文档，它返回一个匹配查询条件的文档对象
		 * findOne() 方法可以接受两个参数：查询对象和实体类
		 * findOne() 方法也可以接受三个参数：查询对象、实体类和集合名（表名）
		 * 此处的需求：
		 * 用户ID 1-100 存储在 userListenProcess_1 集合
		 * 用户ID 101-200 也映射到 1-100 的范围，使用取模运算:
		 * 101 % 100 = 1 → 存储在 userListenProcess_1
		 * 102 % 100 = 2 → 存储在 userListenProcess_2
		 * ...
		 * 200 % 100 = 0 → 特殊处理，存储在 userListenProcess_100
		 * 所以真正的表名是：userListenProcess_1、userListenProcess_2、...、userListenProcess_100
		 */
		// 获取到表名
		String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId); // MongoUtil 是自定义的工具类
		UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collectionName);
		if (userListenProcess == null) {
			// 这个声音以前没有播放过
			return new BigDecimal("0.00");
		}
		return userListenProcess.getBreakSecond();
	}

	/**
	 * 更新MongoDB中用户声音的播放进度
	 *
	 * @param userListenProcessVo
	 */
	@Override
	public void updateListenProcess(UserListenProcessVo userListenProcessVo) {

		Long userId = AuthContextHolder.getUserId();
		// 构建条件对象
		Criteria criteria = Criteria.where("userId").is(userId).and("trackId").is(userListenProcessVo.getTrackId());
		// 构建查询对象
		Query query = new Query(criteria);
		// 开始查询
		String collectionName = MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId);
		UserListenProcess userListenProcess = mongoTemplate.findOne(query, UserListenProcess.class, collectionName);
		if (userListenProcess == null) {
			// 保存声音的播放进度
			userListenProcess = new UserListenProcess();
			userListenProcess.setId(ObjectId.get().toString());
			userListenProcess.setUserId(userId);
			userListenProcess.setAlbumId(userListenProcessVo.getAlbumId());
			userListenProcess.setTrackId(userListenProcessVo.getTrackId());
			userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond());
			userListenProcess.setIsShow(1); // 是否显示
			userListenProcess.setCreateTime(new Date());
			userListenProcess.setUpdateTime(new Date());
			UserListenProcess saveFlag = mongoTemplate.save(userListenProcess, collectionName);
			log.info("保存声音进度：{}", saveFlag != null ? "成功" : "失败");
		} else {
			// 修改声音的播放进度
			userListenProcess.setBreakSecond(userListenProcessVo.getBreakSecond());
			userListenProcess.setUpdateTime(new Date());
			// 只有当文档id存在的时候，save的作用才是修改，反之则是新增一个文档
			UserListenProcess updateFlag = mongoTemplate.save(userListenProcess, collectionName);
			log.info("修改声音进度：{}", updateFlag != null ? "成功" : "失败");
		}

		/**
		 * 更新MySQL中声音以及专辑的播放量（专辑0401，声音0701）
		 * 1、需求：因为声音的播放量会一直增加，所以可能会发送很多重复的消息给下游，下游重复监听到同一个消息。但对于同一个声音，同一天同一个用户不管听多少次，只让这个声音的播放量+1
		 * 2、解决：
		 * 维度一：上游只发一次，下游就只会接收到一次
		 * 维度二：上游无限发，下游做幂等性校验，只处理一次（此处选择维度二）
		 * 3、Redis Bitmap 实现幂等控制
		 * Bitmap Key结构：user:track:日期:trackId
		 * 例如：user:track:2023-11-15:12345
		 * 作用：记录某一天某个声音被哪些用户收听过的标记
		 * userId作为offset：每个用户ID对应bitmap中的一个位(bit)
		 */
		String bitMapKey = "user:track:" + new DateTime().toString("yyyy-MM-dd") + ":" + userListenProcessVo.getTrackId();
		Boolean aBoolean = redisTemplate.opsForValue().getBit(bitMapKey, userId);
		// 如果该用户今天还未听过此声音
		if (!aBoolean) {
			// 在bitmap中标记该用户已收听
			redisTemplate.opsForValue().setBit(bitMapKey, userId, true);
			// 设置key的过期时间为1天（自动清理）
			redisTemplate.expire(bitMapKey, 1, TimeUnit.DAYS);
			// 准备TrackStatMqDto对象。该对象包含了需要发送到MQ的消息内容，用于更新声音的播放统计信息
			TrackStatMqVo trackStatMqVo = userInfoService.prepareTrackStatMqDto(
					userListenProcessVo.getAlbumId(),
					userListenProcessVo.getTrackId(),
					SystemConstant.TRACK_STAT_PLAY,
					1
			);
			rabbitService.sendMessage(MqConst.EXCHANGE_TRACK, MqConst.ROUTING_TRACK_STAT_UPDATE, JSONObject.toJSONString(trackStatMqVo));
		}
	}

	/**
	 * 查询用户最近一次听的专辑对应的声音
	 * @return
	 */
	@Override
	public Map<Object, Object> getLatelyTrack() {

		Long userId = AuthContextHolder.getUserId();
		// 构建条件对象
		Criteria criteria = Criteria.where("userId").is(userId);
		// 封装排序条件
		Sort sort = Sort.by(Sort.Order.desc("updateTime"));
		// 构建查询对象
		Query query = new Query(criteria);
		query.with(sort);
		// 查询
		UserListenProcess userListenProcesses = mongoTemplate.findOne(
				query,
				UserListenProcess.class,
				MongoUtil.getCollectionName(MongoUtil.MongoCollectionEnum.USER_LISTEN_PROCESS, userId)
		);
		if (userListenProcesses == null) {
			return null;
		}
		Map<Object, Object> map = new HashMap<>();
		map.put("albumId", userListenProcesses.getAlbumId());
		map.put("trackId", userListenProcesses.getTrackId());
		return map;
	}
}
